分库分表

名词解释

库:database;表:table;分库分表:sharding

为什么要分库分表

移动互联网时代,海量的用户每天产生海量的数量,比如:

  • 用户表
  • 订单表
  • 交易流水表

以支付宝用户为例,8亿;微信用户更是10亿。订单表更夸张,比如美团外卖,每天都是几千万的订单。淘宝的历史订单总量应该百亿,甚至千亿级别,这些海量数据远不是一张表能Hold住的。事实上MySQL单表可以存储10亿级数据,只是这时候性能比较差,业界公认MySQL单表容量在1KW以下是最佳状态,因为这时它的BTREE索引树高在3~5之间。

既然一张表无法搞定,那么就想办法将数据放到多个地方,目前比较普遍的方案有3个:

  1. 读写分离
  2. 分库分表
  3. NoSQL/NewSQL

说明:只分库,或者只分表,或者分库分表融合方案都统一认为是分库分表方案,因为分库,或者分表只是一种特殊的分库分表而已。NoSQL比较具有代表性的是MongoDB,es。NewSQL比较具有代表性的是TiDB。

读写分离,主从复制

在实际应用中的绝大多数情况下读操作远大于写操作。MySQL提供了读写分离的机制,所有写操作必须对应到主库(Master),读操作可以在主库(Master)和从库(Slave)机器上进行。

主库与从库的结构完全一样,一个主库可以有多个从库,甚至在从库下还可以挂从库,这种一主多从的方式可以有效地提高数据库集群的吞吐量。

在DBA领域一般配置主-主-从或者主-从-从两种部署模型。

所有写操作都先在主库上进行,然后异步更新到从库上,所以从主库同步到从库机器有一定的延迟,当系统很繁忙时,延迟问题会更加严重,从库机器数量的增加也会使这个问题更严重。

此外,主库是集群的瓶颈,当写操作过多时会严重影响主库的稳定性,如果主库挂掉,则整个集群都将不能正常工作。

根据以上特点,我们总结一些最佳实践如下。

  • 当读操作压力很大时,可以考虑添加从库机器来分解大量读操作带来的压力,但是当从库机器达到一定的数量时,就需要考虑分库来缓解压力了。
  • 当写压力很大时,就必须进行分库分表操作了。

可能会因为种种原因,集群中的数据库硬件配置等会不一样,某些性能高,某些性能低,这时可以通过程序控制每台机器读写的比重来达到负载均衡,这需要更加复杂的读写分离的路由规则。

Why Not NoSQL/NewSQL?

首先,为什么不选择第三种方案NoSQL/NewSQL,我认为主要是RDBMS有以下几个优点:

  1. RDBMS生态完善;
  2. RDBMS绝对稳定;
  3. RDBMS的事务特性;

NoSQL/NewSQL作为新生儿,在我们把可靠性当做首要考察对象时,它是无法与RDBMS相提并论的。RDBMS发展几十年,只要有软件的地方,它都是核心存储的首选。

目前绝大部分公司的核心数据都是:RDBMS存储为主,NoSQL/NewSQL存储为辅!互联网公司又以MySQL为主,国企&银行等不差钱的企业以Oracle/DB2为主!NoSQL/NewSQL宣传的无论多牛逼,就现在各大公司对它的定位,都是RDBMS的补充,而不是取而代之!

什么是RDBMS

RDBMS即关系数据库管理系统(Relational Database Management System),是将数据组织为相关的行和列的系统,而管理关系数据库的计算机软件就是关系数据库管理系统,常用的数据库软件有Oracle、SQL Server等。

整体的切分方式

一般就是垂直切分和水平切分,这是一种结果集描述的切分方式,是物理空间上的切分。 我们从面临的问题,开始解决,阐述: 首先是用户请求量太大,我们就堆机器搞定(这不是本文重点)。

然后是单个库太大,这时我们要看是因为表多而导致数据多,还是因为单张表里面的数据多。 如果是因为表多而数据多,使用垂直切分,根据业务切分成不同的库。

如果是因为单张表的数据量太大,这时要用水平切分,即把表的数据按某种规则切分成多张表,甚至多个库上的多张表。

切分方式小结

  • 垂直切分:把单一的表拆分成多个表。
  • 水平切分:根据表中数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库(主机)上。

1. 垂直切分  

垂直分表如果一张表的字段非常多,那么很有可能会引起数据的跨页存储,这会造成数据库额外的性能开销,而垂直分表可以解决这个问题。垂直分表就是将一张表中不常用的字段拆分到另一张表中,从而保证第一章表中的字段较少,避免出现数据库跨页存储的问题,从而提升查询效率。而另一张表中的数据通过外键与第一张表进行关联,如下图所示。

也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。

垂直拆分优点:

  1. 可以使得行数据变小,一个数据块 (Block) 就能存放更多的数据,在查询时就会减少 I/O 次数 (每次查询时读取的 Block 就少)。
  2. 可以达到最大化利用 Cache 的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起。
  3. 便于实现动静分离、冷热分离的数据库表的设计模式。
  4. 数据维护简单。

垂直拆分缺点

  • 主键出现冗余,需要管理冗余列。
  • 会引起表连接 JOIN 操作(增加 CPU 开销)可以通过在业务服务器上进行 join 来减少数据库压力,提高了系统的复杂度。。
  • 依然存在单表数据量过大的问题(需要水平拆分)。
  • 事务处理复杂。

垂直拆分小结:

系统层面的“服务化”拆分操作,能够解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。而数据库层面的拆分,道理也是相通的。与服务的“治理”和“降级”机制类似,我们也能对不同业务类型的数据进行“分级”管理、维护、监控、扩展等。

众所周知,数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈,是大型分布式系统中优化数据库架构的重要手段。

然后,很多人并没有从根本上搞清楚为什么要拆分,也没有掌握拆分的原则和技巧,只是一味的模仿大厂的做法。导致拆分后遇到很多问题(例如:跨库join,分布式事务等)。

关于冷热分离

垂直切分除了用于分解单库单表的压力,也用于实现冷热分离,也就是根据数据的活跃度进行拆分,因为对拥有不同活跃度的数据的处理方式不同。

冷热分离是反范式的,因为按照第三范式,有些字段是需要在一张表里面的,但是冷热分离这种策略会把字段分开。

例如,对配置表的某些字段很少进行修改时,将其放到一个查询性能较高的数据库硬件上;对配置表的其他字段更新频繁时,则将其放到另一个更新性能较高的数据库硬件上。

这里我们再举一个例子:在微博系统的设计中,一个微博对象包括文章标题、作者、分类、创建时间等属性字段,这些字段的变化频率低,查询次数多,叫作冷数据。而博客的浏览量、回复数、点赞数等类似的统计信息,或者别的变化频率比较高的数据,叫作活跃数据或者热数据。

我们把冷热数据分开存放,就叫作冷热分离,在MySQL的数据库中,冷数据查询较多,更新较少,适合用MyISAM引擎,而热数据更新比较频繁,适合使用InnoDB存储引擎,这也是垂直拆分的一种。

我们推荐在设计数据库表结构时,就考虑垂直拆分,根据冷热分离、动静分离的原则,再根据使用的存储引擎的特点,对冷数据可以使用MyISAM,能更好地进行数据查询;对热数据可以使用InnoDB,有更快的更新速度,这样能够有效提升性能。

其次,对读多写少的冷数据可配置更多的从库来化解大量查询请求的压力;对于热数据,可以使用多个主库构建分库分表的结构,请参考下面关于水平切分的内容,后续的三四五章提供了不同的分库分表的具体实施方案。

注意,对于一些特殊的活跃数据或者热点数据,也可以考虑使用Memcache、Redis之类的缓存,等累计到一定的量后再更新数据库,例如,在记录微博点赞数量的业务中,点赞数量被存储在缓存中,每增加1000个点赞,才写一次数据。

2. 水平切分  

如果一张表中的记录数过多(超过1000万条记录),那么会对数据库的读写性能产生较大的影响,虽然此时仍然能够正确地读写,但读写的速度已经到了业务无法忍受的地步,此时就需要使用水平分表来解决这个问题。水平分表是将一张含有很多记录数的表水平切分,拆分成几张结构相同的表。举个例子,假设一张订单表目前存储了2000万条订单的数据,导致数据读写效率极低。此时可以采用水平分表的方式,将订单表拆分成100张结构相同的订单表,分别叫做 order1、order2……order_100。

然后可以根据订单所属用户的 ID 进行哈希取模后均匀地存储在这100张表中,从而每张表中只存储了20万条订单记录,极大提升了订单的读写效率,如下图所示。

针对数据量巨大的单张表(比如订单表),按照某种规则(时间,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。

水平拆分优点

  • 单库单表的数据保持在一定的量级,有助于性能的提高。
  • 切分的表的结构相同,应用层改造较少,只需要增加路由规则即可。
  • 提高了系统的稳定性和负载能力。

水平拆分缺点:

  • 切分后,数据是分散的,很难利用数据库的Join操作,跨库Join性能较差。
  • 分片事务的一致性难以解决。
  • 数据扩容的难度和维护量极大。

综上所述,垂直切分和水平切分的共同点如下:

  • 存在分布式事务的问题。
  • 存在跨节点Join的问题。
  • 存在跨节点合并排序、分页的问题。
  • 存在多数据源管理的问题。

在了解这两种切分方式的特点后,我们就可以根据自己的业务需求来选择,通常会同时使用这两种切分方式,垂直切分更偏向于业务拆分的过程,在技术上我们更关注水平切分的方案。

阿里开发手册关于Join的问题

来自 阿里巴巴Java开发手册,关于这句话的讨论:https://www.zhihu.com/question/56236190

2. 【强制】超过三个表禁止join。需要join的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表join也要注意表索引、SQL性能。

水平切分需要路由

既然数据已经水平切分,那么数据就保存在不同的表中,如果有新的请求要求访问数据库,那么怎么从多个表中找到对应的数据呢?

答案就是:路由。

那么问题来了,什么是路由?

通过分库分表规则查找到对应的表和库的过程叫作路由。简单的说,路由就是映射表,你想查什么,告诉它,它会告诉你数据在哪。
例如,分库分表的规则是user_id % 4,当用户新注册了一个账号时,假设用户的ID是123,我们就可以通过123 % 4 = 3确定此账号应该被保存在User3表中。那么以后当ID为123的用户登录时,我们可通过123 % 4 = 3计算,确定其被记录在User3中。

水平切分规则

规则有很多,具体情况具体分析,这里只列举常见的两种

  1. 按哈希切分
  2. 按时间切分

1)按照哈希切片

数据水平切分后我们希望是一劳永逸或者是易于水平扩展的,所以推荐采用mod 2^n这种一致性Hash。

以统一订单库为例,我们分库分表的方案是32*32的,即通过UserId后四位mod 32分到32个库中,同时再将UserId后四位Div 32 Mod 32将每个库分为32个表,共计分为1024张表。线上部署情况为8个集群(主从),每个集群4个库。

优点:

切片均匀,对数据压力分散效果较好

缺点

查询需要聚合处理,较麻烦

2)按照时间切片

这种切片方式适用于有明显时间特点的数据,例如,距离现在1个季度的数据访问频繁,距离现在两个季度的数据可能没有更新,距离现在3个季度的数据没有查询需求。

针对这种情况,可以通过按照时间进行切片,针对不同的访问频率使用不同档次的硬件资源来节省成本:假设距离现在1个季度的数据访问频率最高,我们就用更好的硬件来运行这个分片;假设距离现在3个季度的数据没有任何访问需求,我们就可以将其整体归档,以方便DBA操作。

按时间切片的一个最大好处就是方便扩容,因为它不需要任何的数据迁移。但是,连续分片有个最大的缺点就是热点问题。按时间切片使得新插入的数据集中在同一个分片上,而往往新插入的数据读写频率较高,因此,读写操作都会集中在最新的分片上,从而无法体现数据分片的优势。

优点:单表大小可控,天然水平扩展。

缺点:无法解决集中写入瓶颈的问题。

小结

在实际的生产实践中,按照哈希切片和按照时间切片都是常用的分库分表方式,并被广泛使用,有时可以结合使用这两种方式,例如:对交易数据先按照季度进行切片,然后对于某一季度的数据按照主键哈希进行切片。

分库分表后的事务处理

主流的分布式事务解决方案有三种:

  1. 两阶段提交协议
  2. 最大努力保证模式
  3. 事务补偿机制。

 1)两阶段提交协议

 

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)

小结

有两个角色:

  1. 协调者(coordinator)
  2. 若干参与者(participant)

两个阶段:

  1. 第一阶段:准备阶段(投票阶段)
  2. 第二阶段:提交阶段(执行阶段)

第一阶段,准备阶段(提交请求阶段)

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作,并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。
  3. 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。

有时候,第一阶段也被称作投票阶段,即各参与者投票是否要继续接下来的提交操作。

第二阶段:提交阶段

成功的情况

当协调者节点从所有参与者节点获得的相应消息都为"同意"时:

  1. 协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送”完成”消息。
  4. 协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。

失败的情况

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

  1. 协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
  2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送”回滚完成”消息。
  4. 协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

举个例子

A组织B、C、D周末看电影。

这里

A:协调者

B、C、D:参与者

2PC分为两个阶段,每一个阶段都有成功、失败两种结果。根据排列组合,共有4中结果。。。。

注意:这里例子A作为协调者,自己本身是不去看电影的。
而且这里B、C、D一定要一起看电影,少一个人都不去看

2PC第一阶段,准备阶段

A:周末有时间吗,一起去看电影

(A开始等待B、C、D的回应)

成功的情况

B:OJBK,没问题

(A收到)

(B把看电影添加到自己日程里,并开始等待C、D回应)

C:有时间,可以去

(A收到)

(C把看电影添加到自己日程里,B、C开始等待D的回应)

D:(D过了一段时间才看到消息)可以

(A收到)

失败的情况

B:OJBK,没问题

(A收到)

(B开始等待C、D回应)

C:有时间,可以去

(A收到)

(B、C开始等待D的回应)

D:不行,我忙

(A收到)

注意:不论第一阶段成功还是失败,都会执行第二阶段。也就是说第一阶段的结果要在第二阶段进行处理

2PC第二阶段

再次注意:这里例子A作为协调者,自己本身是不去看电影的。

而且这里B、C、D一定要一起看电影,少一个人都不去看

成功的情况

成功条件:当协调者节点从所有参与者节点获得的相应消息全部都是"同意"时

B到达电影院

C到达电影院

D到达电影院

结果:B、C、D正常看电影

失败的情况

失败条件:如果任一参与者节点在第一阶段返回的响应消息为"终止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

A通知所有成员:

你们所有成员之前为看电影做的所有准备操作全部回滚,之前啥样,现在还是啥样

结果:所有人员全部事务回滚rollback,回到原始状态

存在问题

通过该例子可以发现,2PC协议存在明显的问题。假如在第一阶段D一直不能回复,那么A、B和C将不得不处于一直等待的状态。并且B和C所持有的资源,即下周末不能安排其它活动,一直不能释放。其它等待该资源释放的活动也将不得不处于等待状态。

等待根本原因还是因为2PC是阻塞执行的,这就导致如果任意一个节点出现问题,其他的节点就会一直等待下去。

并且如果任意节点出现问题,所有的阶段都要事务回滚,代价较大。

基于此,后来有人提出了三阶段提交协议,在其中引入超时的机制,将阶段1分解为两个阶段:在超时发生以前,系统处于不确定阶段;在超市发生以后,系统则转入确定阶段。

2PC协议包含协调者和参与者,并且二者都有发生问题的可能性。假如协调者发生问题,我们可以选出另一个协调者来提交事务。例如,班长组织活动,如果班长生病了,我们可以请副班长来组织。如果协调者出问题,那么事务将不会取消。例如,班级活动希望每个人都能去,假如有一位同学不能去了,那么直接取消活动即可。或者,如果大多数人去的话那么活动如期举行(2PC变种)。为了能够更好地解决实际的问题,2PC协议存在很多的变种,例如:树形2PC协议(或称递归2PC协议)、动态2阶段提交协议(D2PC)等。

2)最大努力保证模式

这是一种非常通用的保证分布式一致性的模式,很多开发人员一直在使用,但是并未意识到这是一种模式。

最大努力保证模式适用于对一致性要求并不十分严格但是对性能要求较高的场景。

具体的实现方法是,在更新多个资源时,将多个资源的提交尽量延后到最后一刻处理,这样的话,如果业务流程出现问题,则所有的资源更新都可以回滚,事务仍然保持一致。

唯一可能出现问题的情况是在提交多个资源时发生了系统问题,比如网络问题等,但是这种情况是非常罕见的,一旦出现这种情况,就需要进行实时补偿,将已提交的事务进行回滚,这和我们常说的TCC模式有些类似。

下面是使用最大努力保证模式的一个样例,在该样例中涉及两个操作,一个是从消息队列消费消息,一个是更新数据库,需要保证分布式的一致性。

(1)开始消息事务。

(2)开始数据库事务。

(3)接收消息。

(4)更新数据库。

(5)提交数据库事务。

(6)提交消息事务。

这时,从第1步到第4步并不是很关键,关键的是第5步和第6步,需要将其放在最后一起提交,尽最大努力保证前面的业务处理的一致性。

到了第5步和第6步,业务逻辑处理完成,这时只可能发生系统错误,如果第5步失败,则可以将消息队列和数据库事务全部回滚,保持一致。如果第5步成功,第6步遇到了网络超时等问题,则这是唯一可能产生问题的情况。

在这种情况下,消息的消费过程并没有被提交到消息队列,消息队列可能会重新发送消息给其他消息处理服务,这会导致消息被重复消费,但是可以通过幂等处理来保证消除重复消息带来的影响。

当然,在使用这种模式时,我们要充分考虑每个资源的提交顺序。我们在生产实践中遇到的一种反模式,就是在数据库事务中嵌套远程调用,而且远程调用是耗时任务,导致数据库事务被拉长,最后拖垮数据库。

因此,上面的案例涉及的是消息事务嵌套数据库事务,在这里必须进行充分评估和设计,才可以规避事务风险。

3)事务补偿机制

显然,在对性能要求很高的场景中,两阶段提交协议并不是一种好方案(原因之一是出点毛病就全部回滚,代价太大),最大努力保证模式也会使多个分布式操作互相嵌套,有可能互相影响。这里,我们给出事务补偿机制,其性能很高,并且能够尽最大可能地保证事务的最终一致性。

事务补偿机制并不苛求系统的实时一致性,只要在一个允许的时间周期内达到最终一致性即可(也就是说是可以慢一点,但是最后数据要一致)。这使得事务补偿机制成为一种可行的方案。

注意:如果采用事务补偿机制,则在遇到问题时,我们需要记录遇到问题的环境、信息、步骤、状态等,为了后续通过重试机制使其达到最终一致性

分库分表两大模式

但是这么多的分库分表中间件全部可以归结为两大类型:

  • CLIENT模式
  • PROXY模式

CLIENT模式代表有阿里的TDDL,开源社区的sharding-jdbc(sharding-jdbc的3.x版本即sharding-sphere已经支持了proxy模式)。架构如下:

PROXY模式代表有阿里的cobar,民间组织的MyCAT。架构如下:

但是,无论是CLIENT模式,还是PROXY模式。几个核心的步骤是一样的:SQL解析,重写,路由,执行,结果归并

分库分表方案产品

应用层依赖类中间件

这类分库分表中间件的特点就是和应用强耦合,需要应用显示依赖相应的jar包(以Java为例),比如知名的TDDL、当当开源的sharding-jdbc、蘑菇街的TSharding、携程开源的Ctrip-DAL、支付宝开源但比较低调的zdal等。

此类中间件的基本思路就是重新实现JDBC的API,通过重新实现DataSource、PrepareStatement等操作数据库的接口,让应用层在基本(注意:这里用了基本)不改变业务代码的情况下透明地实现分库分表的能力。中间件给上层应用提供熟悉的JDBC API,内部通过SQL解析、SQL重写、SQL路由等一系列的准备工作获取真正可执行的SQL,然后底层再按照传统的方法(比如数据库连接池)获取物理连接来执行SQL,最后把数据结果合并处理成ResultSet返回给应用层。

此类中间件的优点很明显,就是无需额外部署,只要和应用绑定一起发布即可,但是缺点也很明显,就是不能跨语言,比如Java写的sharding-jdbc显然不能用在C#项目中,所以携程的dal也要重新写一套C#的客户端。

中间层代理类中间件

这类分库分表中间件的核心原理是在应用和数据库的连接之间搭起一个代理层,上层应用以标准的MySQL协议来连接代理层,然后代理层负责转发请求到底层的MySQL物理实例,这种方式对应用只有一个要求,就是只要用MySQL协议来通信即可,所以用MySQL Workbench这种纯的客户端都可以直接连接你的分布式数据库,自然也天然支持所有的编程语言。比较有代表性的产品有开创性质的Amoeba、阿里开源的Cobar、社区发展比较好的Mycat 等。

在技术实现上除了和应用层依赖类中间件基本相似外,代理类的分库分表产品必须实现标准的MySQL协议,某种意义上讲数据库代理层转发的就是MySQL协议请求,就像Nginx转发的是Http协议请求。

上述无论哪种类型的产品,除了实现分库分表这一主要功能外,都会额外实现一些其他很有实用价值的功能,比如读写分离、负载均衡等。

生成全局唯一ID

在很多中小项目中,我们往往直接使用数据库自增特性来生成主键ID,这样确实比较简单。而在分库分表的环境中,数据分布在不同的分片上,不能再借助数据库自增长特性直接生成,否则会造成不同分片上的数据表主键会重复。简单介绍下使用和了解过的几种ID生成算法。

方法有很多,本文总结以下方法

  1. 利用数据库自增ID
  2. UUID
  3. 利用数据库集群并设置相应的步长(Flickr方案)
  4. 类Snowflake方案
  5. Redis生成ID

利用数据库自增ID

以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

begin;
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;

这种方案的优缺点如下:

优点:

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
  • ID号单调自增,可以实现一些对ID有特殊要求的业务。

缺点:

  • 强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
  • ID发号性能瓶颈限制在单台MySQL的读写性能。

UUID

UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前为止业界一共有5种方式生成UUID,详情见IETF发布的UUID规范 A Universally Unique IDentifier (UUID) URN Namespace

优点:

  • 性能非常高:本地生成,没有网络消耗。

缺点:

  • 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
  • ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:

① MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID不符合要求。

All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index. If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.

② 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

利用数据库集群并设置相应的步长(Flickr方案)

flickr使用的一种主键生成测策略。
flickr这一方案的整体思想是:建立两台以上的数据库ID生成服务器,每个服务器都有一张记录各表当前ID的Sequence表,但是Sequence中ID增长的步长是服务器的数量,起始值依次错开,这样相当于把ID的生成散列到了每个服务器节点上。例如:如果我们设置两台数据库ID生成服务器,那么就让一台的Sequence表的ID起始值为1,每次增长步长为2,另一台的Sequence表的ID起始值为2,每次增长步长也为2,那么结果就是奇数的ID都将从第一台服务器上生成,偶数的ID都从第二台服务器上生成,这样就将生成ID的压力均匀分散到两台服务器上,同时配合应用程序的控制,当一个服务器失效后,系统能自动切换到另一个服务器上获取ID,从而保证了系统的容错。

优点:高可用、ID较简洁。
缺点:需要单独的数据库集群。

类Snowflake方案

这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图(图片来自网络)所示:

41-bit的时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

这种方式的优缺点是:

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

应用举例Mongdb objectID

MongoDB官方文档 ObjectID可以算作是和snowflake类似方法,通过“时间+机器码+pid+inc”共12个字节,通过4+3+2+3的方式最终标识成一个24长度的十六进制字符。

Redis生成ID

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用于生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。

关于全局ID生成详细可看

https://juejin.im/entry/57fe37eeda2f60004fb45723

https://tech.meituan.com/MT_Leaf.html

参考

https://dbaplus.cn/news-141-2017-1.html

http://www.cnblogs.com/405845829qq/p/7552736.html

https://mp.weixin.qq.com/s/DahF7Epx6MG95ZbxrMka2Q

https://gitbook.cn/books/5ab8a5ca6fa0783769f10ac1/index.html

https://tech.meituan.com/dianping_order_db_sharding.html

https://blog.csdn.net/KingCat666/article/details/78324678

http://www.ywnds.com/?p=7239

https://juejin.im/post/5bf778ef5188251b8a26ed8b

https://www.hollischuang.com/archives/681

https://zh.wikipedia.org/wiki/%E4%BA%8C%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值